﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.IO;

namespace BitTorrentSharp.Client
{
	using Peers;

	/// <summary>
	/// Helper class for reading and writing messages. This class is a static class.
	/// </summary>
	public static class StreamHelper
	{
		/// <summary>
		/// Send the message.
		/// </summary>
		/// <param name="stream">The stream where the message should be writen.</param>
		/// <param name="message">The message to be writen.</param>
		public static void SendMessage(this Stream stream, PeerMessage message)
		{
			if (stream == null)
			{
				throw new ArgumentNullException("stream");
			}

			if (message == null)
			{
				throw new ArgumentNullException("message");
			}

			Contract.EndContractBlock();

			var rawMessage = message.ToRawMessage();

			WriteRawMessage(stream, rawMessage);
		}

		/// <summary>
		/// Read a raw message from a stream.
		/// </summary>
		/// <param name="stream">The stream where the message should be read.</param>
		/// <returns>The raw message read.</returns>
		/// <remarks>This method will generate trace information.</remarks>
		public static RawMessage ReadRawMessage(this Stream stream)
		{
			if (stream == null)
			{
				throw new ArgumentNullException("stream");
			}

			Contract.EndContractBlock();

			var length = ReadInt32BigEndian(stream);
			var data = ReadBytes(stream, length);

			var result = new RawMessage(data);

			// Output Trace information.
			Trace.TraceInformation("Raw message read, length = {0}.", length);

			return result;
		}

		/// <summary>
		/// Write a raw message to a stream.
		/// </summary>
		/// <param name="stream">The stream where the message should be writen.</param>
		/// <param name="message">The message to be writen.</param>
		/// <remarks>This method will generate trace information.</remarks>
		public static void WriteRawMessage(this Stream stream, RawMessage message)
		{
			if (stream == null)
			{
				throw new ArgumentNullException("stream");
			}
			if (message == null)
			{
				throw new ArgumentNullException("message");
			}

			var perfix = BitConverter.GetBytes(message.Length).EnsureBigEndian();
			var content = message.Data;

			stream.Write(perfix, 0, perfix.Length);
			stream.Write(content, 0, content.Length);

			// Output Trace information.
			Trace.TraceInformation("Raw message writen, length = {0}.", message.Length);
		}

		/// <summary>
		/// Read a handshake message from a straem.
		/// </summary>
		/// <param name="stream">The stream where the message should be read.</param>
		/// <returns>The raw message read.</returns>
		/// <remarks>This method will generate trace information.</remarks>
		public static HandshakeMessage ReadHandshakeMessage(this Stream stream)
		{
			if (stream == null)
			{
				throw new ArgumentNullException("stream");
			}

			Contract.EndContractBlock();

			var result = new HandshakeMessage();
			result.LeadingMessage = ReadStringWithLengthPerfix(stream);
			result.Reversed = ReadInt64BigEndian(stream);
			result.Sha1Hash = ReadBytes(stream, 20);
			result.PeerId = ReadBytes(stream, 20);

			Trace.TraceInformation("Handshake message read, leading message = {0}, reversed = {1:x}, SHA1 = {2}, Peer ID = {3}", result.LeadingMessage, result.Reversed, BitConverter.ToString(result.Sha1Hash), BitConverter.ToString(result.PeerId)); 

			return result;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="stream"></param>
		public static void WriteHandshakeMessage(this Stream stream)
		{
		}

		/// <summary>
		/// Read an ASCII string with single-byte length perfix.
		/// </summary>
		/// <param name="stream">The stream where the string should be read.</param>
		/// <returns>The decoded ASCII string.</returns>
		private static string ReadStringWithLengthPerfix(this Stream stream)
		{
			var length = stream.ReadByte();

			if (length < 0)
			{
				throw new EndOfStreamException("Unexcepted stream end. Need one byte for string length.");
			}

			return ReadString(stream, length);
		}


		/// <summary>
		/// Read an ASCII string with the speficied byte count from a stream.
		/// </summary>
		/// <param name="stream">The stream where the string should be read.</param>
		/// <param name="lengthInByte">The length of the stream in byte.</param>
		/// <returns>The decoded ASCII string.</returns>
		private static string ReadString(this Stream stream, int lengthInByte)
		{
			var bytes = ReadBytes(stream, lengthInByte);

			// TODO: Consider to use UTF-8 Encoding in order to enhance the compatibility.
			return Encoding.ASCII.GetString(bytes);
		}

		/// <summary>
		/// Read a big-endian 64-bit integer from a stream.
		/// </summary>
		/// <param name="stream">The stream where the integer should be read.</param>
		/// <returns>The 32-bit integer read.</returns>
		private static long ReadInt64BigEndian(this Stream stream)
		{
			// Length of integer.
			var length = 8;

			var bytes = ReadBytes(stream, length);

			bytes.EnsureBigEndian();

			return BitConverter.ToInt64(bytes, 0);
		}

		/// <summary>
		/// Read a big-endian 32-bit integer from a stream.
		/// </summary>
		/// <param name="stream">The stream where the integer should be read.</param>
		/// <returns>The 32-bit integer read.</returns>
		private static int ReadInt32BigEndian(this Stream stream)
		{
			// Length of integer.
			var length = 4;

			var bytes = ReadBytes(stream, length);

			bytes.EnsureBigEndian();

			return BitConverter.ToInt32(bytes, 0);
		}

		/// <summary>
		/// Read bytes from a stream.
		/// </summary>
		/// <param name="stream">The stream where the data should be read.</param>
		/// <param name="length">The excepted length of the data.</param>
		/// <returns>The bytes read.</returns>
		private static byte[] ReadBytes(this Stream stream, int length)
		{
			var result = new byte[length];

			if (stream.Read(result, 0, result.Length) != length)
			{
				throw new EndOfStreamException("Unexcepted stream end. Need at least 4 bytes.");
			}

			return result;
		}
	}
}
